﻿using System;
using System.Collections.Generic;
using NCommet.Core;
using NCommet.Core.Agents;
using System.Reflection;
using log4net;

namespace NCommet.Modules.InMemorySorter
{
    /// <summary>
    /// An implementation of <see cref="NCommet.Core.Agents.ISorter"/>
    /// </summary>
    public class InMemorySorter : ISorter
    {
        /// <summary>
        /// A struct containing a content base object and the value of its indexable attribute, upon which the comparison is performed.
        /// </summary>
        struct ContentAndValue
        {
            public static readonly ContentAndValue Empty = new ContentAndValue(null, null);

            public ContentBase content;
            public object value;

            public ContentAndValue(ContentBase content, object value)
            {
                this.content = content;
                this.value = value;
            }

            /// <summary>
            /// A generic comparator between two <see cref="ContentAndValue"/> objects. These objects have a value of type T in their indexable attribute.
            /// The comparison returns:
            /// <list type="number">
            /// <item>-1, if (a == null AND b != null) OR (a != null AND b != null AND a.value is LESS THAN b.value)</item>
            /// <item>0, if (a == null AND b == null) OR (a != null AND b != null AND a.value EQUALS b.value)</item>
            /// <item>1, if (a != null AND b == null) OR (a != null AND b != null AND a.value is GREATER THAN b.value)</item>
            /// </list>
            /// </summary>
            /// <typeparam name="T">The type of the indexable attribute's value.</typeparam>
            /// <param name="a">The first ContentAndValue object.</param>
            /// <param name="b">The second ContentAndValue object.</param>
            /// <returns>The result of the comparison between <paramref name="a"/> and <paramref name="b"/></returns>
            public static int TypeCompare<T>(ContentAndValue a, ContentAndValue b) where T : IComparable<T>
            {
                int result = 0;

                if (a.value == null)
                    result = (b.value == null) ? 0 : -1;
                else
                {
                    if (b.value == null)
                        result = 1;
                    else
                    {
                        result = ((T)a.value).CompareTo((T)b.value);
                        if (result == 0)
                            result = DefaultOrderCompare(a, b);
                    }
                }
                LogManager.GetLogger("InMemorySorter").DebugFormat("Compared {0} to {1}, {5}, ids {2} to {3}, result={4}", a.value, b.value, a.content.Id, b.content.Id, result, a.value == b.value);
                return result;
            }

            /// <summary>
            /// A comparator between two <see cref="ContentAndValue"/> objects with a DateTime value in their indexable attribute.
            /// The comparison returns:
            /// <list type="number">
            /// <item>-1, if (a == null AND b != null) OR (a != null AND b != null AND a.value is LESS THAN b.value)</item>
            /// <item>0, if (a == null AND b == null) OR (a != null AND b != null AND a.value EQUALS b.value)</item>
            /// <item>1, if (a != null AND b == null) OR (a != null AND b != null AND a.value is GREATER THAN b.value)</item>
            /// </list>
            /// </summary>            
            /// <param name="a">The first ContentAndValue object.</param>
            /// <param name="b">The second ContentAndValue object.</param>
            /// <returns>The result of the comparison between <paramref name="a"/> and <paramref name="b"/></returns>
            public static int DateTimeCompare(ContentAndValue a, ContentAndValue b)
            {
                int result = 0;

                if (a.value == null)
                    result = (b.value == null) ? 0 : -1;
                else
                {
                    if (b.value == null)
                        result = 1;
                    else
                    {
                        a.value = TrimMSecs((DateTime)a.value);
                        b.value = TrimMSecs((DateTime)b.value);
                        result = ((DateTime)a.value).CompareTo((DateTime)b.value);
                        if (result == 0)
                            result = DefaultOrderCompare(a, b);
                    }
                }
                LogManager.GetLogger("InMemorySorter").DebugFormat("Compared {0} to {1}, {5}, ids {2} to {3}, result={4}", a.value, b.value, a.content.Id, b.content.Id, result, (DateTime)a.value == (DateTime)b.value);
                return result;
            }

            public static int DefaultOrderCompare(ContentAndValue a, ContentAndValue b)
            {
                if (a.content == null || a.content.Container == null)
                    return (b.content == null || b.content.Container == null) ? 0 : -1;
                else
                {
                    if (b.content == null || b.content.Container == null)
                        return 1;
                    return DefaultOrderOfItem(a.content.Container).CompareTo(DefaultOrderOfItem(b.content.Container));
                }
            }

            public static DateTime TrimMSecs(DateTime dt)
            {
                return new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second, 0);
            }

            public static int DefaultOrderOfItem(Item item)
            {
                if (item == null || item.Parent == null)
                    return 0;
                int result = item.Parent.Children.IndexOf(item);
                LogManager.GetLogger("InMemorySorter").DebugFormat("DefaultOrderOfItem {0}={1}", item.Id, result);
                return result;
            }
        }

        /// <summary>
        /// This method does the following:        
        /// 1.) Retrieves the item with the specified id. If it is null, it returns an empty <see cref="ContentAndValue"/>
        /// 2.) Retrieves the content of the item. If it is null, it returns an empty <see cref="ContentAndValue"/>        
        /// 3.) Retrieves all the properties of the content object and for each one of them checks if they have an attribute of type <see cref="IndexableAttribute"/>
        /// For all these properties, it checks if their indexable attribute key equals <paramref name="Key"/> and if it does it returns a new <see cref="ContentAndValue"/>                
        /// </summary>
        /// <param name="id">The id of the item.</param>
        /// <param name="Key">The sorting key.</param>
        /// <returns>A <see cref="ContentAndValue"/> object.</returns>
        private ContentAndValue FromId(int id, int Key)
        {
            Item it = Item.Get(id, true);
            if (it == null)
                return ContentAndValue.Empty;

            ContentBase content = it.Content;
            if (content == null)
                return ContentAndValue.Empty;

            Type t = content.GetType();
            foreach (PropertyInfo pi in t.GetProperties())
            {
                object[] attrs = pi.GetCustomAttributes(typeof(IndexableAttribute), false);
                if (attrs != null && attrs.Length == 1)
                {
                    IndexableAttribute idxAttr = attrs[0] as IndexableAttribute;
                    if (idxAttr != null && idxAttr.Key == Key)
                        return new ContentAndValue(content, pi.GetValue(content, null));
                }
            }
            return ContentAndValue.Empty;
        }

        /// <summary>
        /// Finds the appropriate method to be used for comparison based on the value type of the indexable attribute.
        /// </summary>
        /// <param name="cav">The given <see cref="ContentAndValue"/> object.</param>
        /// <returns>The comparison method to be used.</returns>
        private Comparison<ContentAndValue> GuessComparison(ContentAndValue cav)
        {
            if (cav.value is DateTime)
                return ContentAndValue.DateTimeCompare;
            else if (cav.value is int)
                return ContentAndValue.TypeCompare<int>;
            else if (cav.value is string)
                return ContentAndValue.TypeCompare<string>;
            else
                return null;
        }

        private static readonly ILog log = LogManager.GetLogger("InMemorySorter");

        #region ISorter Members

        /// <summary>
        /// Sorts a list of items in ascending order. The comparison is made based on the field of the item's content that's marked with
        /// <see cref="NCommet.Core.IndexableAttribute" /> of the specified <c>Key</c>.
        /// The method does the following:
        /// <list type="number">
        /// <item>Fills a list of <see cref="ContentAndValue"/> using the <paramref name="ItemsToSort"/></item>
        /// <item>Searches for the appropriate comparator i.e. datetime, int and string.</item>
        /// <item>Sorts the list of <see cref="ContentAndValue"/> using the comparator found.</item>
        /// <item>Returns an enumerable of the sorted items (in ascending order).</item>
        /// </list>
        /// </summary>
        /// <param name="ItemsToSort">A list of item ids</param>
        /// <param name="Key">The key to use for comparison. All items must have a content whose type
        /// contains a property marked with <see cref="NCommet.Core.IndexableAttribute" /> of that key.
        /// </param>
        /// <returns>An enumerable of the sorted items (in ascending order).</returns>
        public IEnumerable<Item> Sort(IList<int> ItemsToSort, int Key)
        {
            log.Debug("Begin Sort");

            List<ContentAndValue> properties = new List<ContentAndValue>();
            if (ItemsToSort != null)
                foreach (int id in ItemsToSort)
                    properties.Add(FromId(id, Key));


            Comparison<ContentAndValue> x = null;
            for (int i = 0; x == null && i < properties.Count; i++)
                x = GuessComparison(properties[i]);

            if (x != null)
                properties.Sort(x);

            log.DebugFormat("End Sort, count={0}", properties.Count);

            foreach (ContentAndValue cav in properties)
                yield return cav.content.Container;
        }

        public IList<int> SortToIds(IList<int> ItemsToSort, int Key)
        {
            log.Debug("Begin Sort");

            List<ContentAndValue> properties = new List<ContentAndValue>();
            if (ItemsToSort != null)
                foreach (int id in ItemsToSort)
                    properties.Add(FromId(id, Key));


            Comparison<ContentAndValue> x = null;
            for (int i = 0; x == null && i < properties.Count; i++)
                x = GuessComparison(properties[i]);

            if (x != null)
                properties.Sort(x);

            log.DebugFormat("End Sort, count={0}", properties.Count);

            return properties.ConvertAll<int>(delegate(ContentAndValue cav)
            {
                return cav.content.Id;
            });
        }

        #endregion
    }
}
